﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using RayDen.Library.Components.Surface;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Data;
using RayDen.Library.Data.Imaging;
using RayDen.Library.Data.Import.OBJLoader;
using RayDen.Library.Data.Scene.SCFF;
using RayDen.Library.Entity.Frames;
using RayDen.Library.Entity.Interface;
using RayDen.Library.Entity.Renderer;
using RayDen.Library.Entity.Scene;
using RayDen.RayEngine.Core.Components;
using RayDen.RayEngine.Core.Interface;
using RayDen.RayEngine.Data;
using RayDen.RayEngine.IntersectionEngines;

namespace RayDen.RayEngine.Scene
{

    #region Entity Factories

    public class FactoryContext
    {
        public Dictionary<string, object> GlobalParams;

        public FactoryContext()
        {
            GlobalParams = new Dictionary<string, object>();
        }
    }

    public interface IEntityFactory<TEntity>
    {
        FactoryContext Context { get; set; }
        TEntity Create(ConfigEntity entityDescription);
        ConfigEntity Serialize(TEntity element);
    }

    public class LightFactory : IEntityFactory<ILight>
    {
        public FactoryContext Context { get; set; }

        private LightEmissionProfilesManager lm = LightEmissionProfilesManager.Instance;

        public ILight Create(ConfigEntity entityDescription)
        {
            string profile = entityDescription.Get("profile", "default");
            switch (entityDescription.Type)
            {
                case "spheric":
                    return new SphereAreaLight()
                    {
                        Position = ValueReader.Convert<Point>(entityDescription["position"].ToString()),
                        Radius = ValueReader.Convert<float>(entityDescription["radius"].ToString()),
                        Profile = lm[profile],
                    };

                case "point":
                    return new PointLight(ValueReader.Convert<Point>(entityDescription["position"].ToString()),
                                          RgbSpectrum.UnitSpectrum(), SampledSpectrum.UnitSpectrum()) { Profile = lm[profile] };
                case "area":
                    return new MeshLight()
                        {
                            Profile = lm[profile],
                            MeshName = entityDescription.Name,
                            LightName = entityDescription.Name,
                        };
                case "infinite":
                    switch (entityDescription["type"].ToString())
                    {
                        case "envmap":
                        default:
                            return new InfiniteLight(entityDescription["filepath"].ToString().Trim('"').Replace(',', ' ')) { Profile = lm[profile] };
                        case "sunsky":
                            return new SunLight(ValueReader.Convert<float>(entityDescription["turbulence"].ToString()),
                                                ValueReader.Convert<Vector>(entityDescription["sundirection"].ToString()));
                        case "sky":
                            return new SkyLight(ValueReader.Convert<float>(entityDescription["turbulence"].ToString()),
                                                ValueReader.Convert<Vector>(entityDescription["sundirection"].ToString()));
                    }
            }

            return null;
        }


        public ConfigEntity Serialize(ILight element)
        {
            var result = new ConfigEntity() { Name = element.Name };
            if (element is PointLight)
            {
                var pt = element as PointLight;
                result.Type = "point";
                result["position"] = pt.Position.ToString();
            }
            else if (element is MeshLight)
            {
                var pt = element as MeshLight;
                result.Type = "area";
                result["meshname"] = pt.MeshName;
                result["gain"] = pt.gain.ToString(",");
            }
            else if (element is InfiniteLight)
            {
                var pt = element as InfiniteLight;
                result.Type = "infinite";
                result["type"] = "envmap";
                result["filepath"] = pt.Sampler.FileName;

            }
            else if (element is SunLight)
            {
                var pt = element as SunLight;
                result.Type = "infinite";
                result["type"] = "sun";
                result["turbulence"] = pt.Turbidity.ToString();
                result["sundir"] = pt.SunDir.ToString();

            }
            else if (element is SkyLight)
            {
                var pt = element as SkyLight;
                result.Type = "infinite";
                result["type"] = "sky";
                result["turbulence"] = pt.Turbidity.ToString();
                result["sundir"] = pt.SunDir.ToString();

            }
            else if (element is TriangleLight)
            {
                var pt = element as TriangleLight;
                result.Type = "area";
                result["meshname"] = pt.Owner.MeshName;
                result["gain"] = pt.Gain.ToString(",");
            }
            else if (element is SphereAreaLight)
            {
                var pt = element as SphereAreaLight;
                result.Type = "spheric";
                result["position"] = pt.Position.ToString();
                result["radius"] = pt.Radius.ToString();
            }

            result["profile"] = element.Profile.Name;

            return result;
        }
    }

    public class CameraFactory : IEntityFactory<ICamera>
    {
        public FactoryContext Context { get; set; }

        public ICamera Create(ConfigEntity entityDescription)
        {
            var width = (int)Context.GlobalParams["width"];
            var height = (int)Context.GlobalParams["height"];

            switch (entityDescription.Type)
            {
                case "fa":
                case "finiteapperture":
                    return new FiniteAppertureCamera(
                                    ValueReader.Convert<Point>(entityDescription["position"].ToString()),
                                    ValueReader.Convert<Vector>(entityDescription["target"].ToString()),
                                    ValueReader.Convert<Vector>(entityDescription["up"].ToString()),
                                    width,
                                    height,
                                    ValueReader.Convert<float>(entityDescription["fov"].ToString())
                                );

                case "pinhole":
                    return new BasicPerspectiveCamera(
                                        ValueReader.Convert<Point>(entityDescription["position"].ToString()),
                                        ValueReader.Convert<Vector>(entityDescription["target"].ToString()),
                                        ValueReader.Convert<Vector>(entityDescription["up"].ToString()),
                                        width,
                                        height,
                                        ValueReader.Convert<float>(entityDescription["fov"].ToString())
                                    );

            }
            return null;
        }

        public ConfigEntity Serialize(ICamera element)
        {
            string cameraName = "camera1";
            string type = "pinhole";
            if (element is FiniteAppertureCamera)
            {
                type = "finiteapperture";

            }

            return new ConfigEntity(cameraName, type, new Dictionary<string, object>()
                    {
                        {"position", element.Position.ToString()},
                        {"target", element.Target.ToString()},
                        {"up", element.Up.ToString()},
                        {"fov", element.Fov.ToString()},                        
                    });
        }
    }

    public class MaterialFactory : IEntityFactory<ISurfaceMaterial>
    {
        public FactoryContext Context { get; set; }
        static readonly Dictionary<string, BrdfClass> classMap = new Dictionary<string, BrdfClass>()
            {
                {"matte", BrdfClass.DiffuseLambert},
                {"alloy", BrdfClass.Alloy},
                {"metal", BrdfClass.GlossyMetal},
                {"specular", BrdfClass.Mirror},
                {"measured", BrdfClass.Measured},
                {"on", BrdfClass.OrenNayar},
                {"aphong", BrdfClass.AnisotropicPhong},
                {"glass", BrdfClass.Glass},
                {"archglass", BrdfClass.ArchGlass},
                {"distr", BrdfClass.Distribution},
            };

        static readonly Dictionary<BrdfClass, string> nameMap = new Dictionary<BrdfClass, string>()
            {
                { BrdfClass.DiffuseLambert  ,"matte"            },
                { BrdfClass.Alloy           ,"alloy"            },
                { BrdfClass.GlossyMetal     ,"metal"            },
                { BrdfClass.Mirror          ,"specular"         },
                { BrdfClass.Measured        ,"measured"         },
                { BrdfClass.OrenNayar       ,"on"               },
                { BrdfClass.AnisotropicPhong,"aphong"           },
                { BrdfClass.Glass           ,"glass"            },
                { BrdfClass.ArchGlass       ,"archglass"       },
                {BrdfClass.Distribution     ,"distr"        },
            };

        public ISurfaceMaterial Create(ConfigEntity entityDescription)
        {
            var mi = new FrameMaterialInfo()
                {
                    MaterialName = entityDescription.Name,
                    BrdfClass = classMap[entityDescription.Type.ToLowerInvariant()].ToString()
                };

            mi.Diffuse = ValueReader.Convert(entityDescription.Get("diffuse", string.Empty), new RgbSpectrum(0.45f));
            mi.DiffuseTexture = entityDescription.Get("diffusemap", string.Empty);

            mi.Glossy = ValueReader.Convert(entityDescription.Get("glossy", string.Empty), new RgbSpectrum(0.45f));
            mi.Specular = ValueReader.Convert(entityDescription.Get("specular", string.Empty), new RgbSpectrum(0.0f));
            mi.BumpTexture = entityDescription.Get("bumpmap", string.Empty);
            mi.AlphaTexture = entityDescription.Get("alphamap", string.Empty);
            mi.DataFile = entityDescription.Get("datafile", string.Empty);

            return SurfaceMaterials.CreateFrameMaterial(mi);

        }


        public ConfigEntity Serialize(ISurfaceMaterial elem)
        {
            var element = (SurfaceMaterial)elem;
            var result = new ConfigEntity() { Name = element.Name, Type = nameMap[element.Class] };
            result["diffuse"] = element.First.MaterialData.DiffuseReflectance.ToString();
            result["glossy"] = element.First.MaterialData.GlossReflectance.ToString();
            result["specular"] = element.First.MaterialData.SpecularReflectance.ToString();

            return result;
        }
    }

    public class LightProfileFactory : IEntityFactory<EmissionProfile>
    {
        public FactoryContext Context { get; set; }

        public EmissionProfile Create(ConfigEntity entityDescription)
        {
            var result = new EmissionProfile() { Name = entityDescription.Name };
            var spdFile = entityDescription.Get<string>("spdfile", null);
            var emission = entityDescription.GetValue("gain", new RgbSpectrum(1.0f));
            var emissionMap = entityDescription.Get<string>("emissionMap", null);
            if (!string.IsNullOrWhiteSpace(spdFile))
            {
                return new EmissionProfile(spdFile) { Name = entityDescription.Name };
            }
            if (!string.IsNullOrWhiteSpace(emissionMap))
            {
                result.EmissionTexture = ImageFactory.FreeImageLoadBitmap(emissionMap);
            }
            else
                result.EmissionSpectra = emission.ToArray();

            return result;
        }

        public ConfigEntity Serialize(EmissionProfile element)
        {
            return new ConfigEntity(element.Name, "profile", new Dictionary<string, object>()
                {
                    {"emission", element.EmissionSpectra.Aggregate(0,",") },
                    {"spdFile", element.SpdFile},
                    {"emissionMap", element.EmissionTexture.ToString() }
                });
        }
    }


    #endregion

    public class SceneConfigurationLoader
    {
        private int width, height;

        private SceneConfiguration configuration;
        internal QualitySettingsInfo qualitySettings;
        internal OutputSettingsInfo outputSettings;
        private FactoryContext context;

        private RgbSpectrum lightGain;
        private int maxPathDepth, pathBufferSize;

        private CameraFactory cameraFactory;
        private LightProfileFactory profileFactory;

        private MaterialFactory materialFactory;
        private LightFactory lightFactory;


        public OptimizedTextureAtlas Textures;
        public Dictionary<TextureType, int>[] texturesMap;

        internal SceneGeometryInfo SceneGeo;
        internal List<ISurfaceMaterial> Materials;


        public SceneConfiguration Config
        {
            get { return configuration; }
        }



        public SceneConfigurationLoader()
        {
            configuration = new SceneConfiguration();
            qualitySettings = new QualitySettingsInfo();
            outputSettings = new OutputSettingsInfo();
            context = new FactoryContext();

            cameraFactory = new CameraFactory { Context = context };
            lightFactory = new LightFactory { Context = context };
            materialFactory = new MaterialFactory { Context = context };
            profileFactory = new LightProfileFactory { Context = context };

        }

        public void Load(string path, RayEngineScene scene)
        {
            configuration.Load(path);
            width = ValueReader.Convert<int>(configuration.ReadFrame("width").ToString());
            height = ValueReader.Convert<int>(configuration.ReadFrame("height").ToString());
            lightGain = ValueReader.Convert(configuration.ReadFrame("lightgain"), new RgbSpectrum(1.6f));
            outputSettings.Width = width;
            outputSettings.Height = height;
            outputSettings.BitsPerPixel = 32;
            qualitySettings.SamplesPerPixel = ValueReader.Convert<int>(configuration.ReadFrame("samplesperpixel").ToString());
            qualitySettings.ShadowRaysPerLight = ValueReader.Convert<int>(configuration.ReadFrame("shadowraysperlight").ToString());
            qualitySettings.TextureSamplingQuality = (TextureSamplingQuality)ValueReader.Convert<int>(configuration.ReadFrame("texturesampling").ToString());
            qualitySettings.SuperSamplingSize = ValueReader.Convert<int>(configuration.ReadFrame("supersampling"), 1);
            qualitySettings.LightSamplingStrategy = (LightSamplingStrategy)ValueReader.Convert<int>(configuration.ReadFrame("lightstrategy"), 1);
            scene.RussianRuletteImportanceCap = ValueReader.Convert<float>(configuration.ReadFrame("rrimportancecap"), 0.75f);
            pathBufferSize = ValueReader.Convert<int>(configuration.ReadFrame("pathbuffersize").ToString(), 65535);
            maxPathDepth = ValueReader.Convert<int>(configuration.ReadFrame("maxpathdepth").ToString(), 10);
            context.GlobalParams.Add("width", width);
            context.GlobalParams.Add("height", height);

            scene.TextureSamplingQuality = qualitySettings.TextureSamplingQuality;
            scene.LightSamplingStrategy = qualitySettings.LightSamplingStrategy;

            // Camera loading

            if (configuration.FiniteAppertureCameras.Any<ConfigEntity>())
            {
                var cameraData = configuration.FiniteAppertureCameras.Cast<KeyValuePair<string, Dictionary<string, object>>>().First();
                scene.Camera = new FiniteAppertureCamera(
                                    ValueReader.Convert<Point>(cameraData.Value["position"].ToString()),
                                    ValueReader.Convert<Vector>(cameraData.Value["target"].ToString()),
                                    ValueReader.Convert<Vector>(cameraData.Value["up"].ToString()),
                                    width,
                                    height,
                                    ValueReader.Convert<float>(cameraData.Value["fov"].ToString())
                                );
            }

            if (configuration.PinholeCameras.Any<ConfigEntity>())
            {
                var cameraData = configuration.PinholeCameras.Cast<KeyValuePair<string, Dictionary<string, object>>>().First();
                scene.Camera = new BasicPerspectiveCamera(
                                    ValueReader.Convert<Point>(cameraData.Value["position"].ToString()),
                                    ValueReader.Convert<Vector>(cameraData.Value["target"].ToString()),
                                    ValueReader.Convert<Vector>(cameraData.Value["up"].ToString()),
                                    width,
                                    height,
                                    ValueReader.Convert<float>(cameraData.Value["fov"].ToString())
                                );
            }


            var directory = configuration.ReadFrame("directory").ToString();
            var frameName = configuration.ReadFrame("name").ToString();

            if (!string.IsNullOrWhiteSpace(directory))
            {
                Directory.SetCurrentDirectory(directory);
            }

            var objFileName = configuration.ReadScene("mesh", "objfilename").ToString();
            var mtlFileName = configuration.ReadScene("mesh", "mtlfilename").ToString();
            var invertNormals = ValueReader.Convert(configuration.ReadScene("mesh", "invertnormals").ToString(), false);

            var objLoader = new GlobalIndexObjLoader();
            var mtlLoader = new MaterialLoader();

            var geoInfo = objLoader.Load(objFileName.Trim('\r', 'n', '"'), invertNormals);
            var mtls = mtlLoader.LoadMaterials(mtlFileName.Trim('\r', 'n', '"'));

            this.SceneGeo = geoInfo;
            this.MaterialInfos = mtls;

            scene.AreaLightAsMesh = ValueReader.Convert(configuration.ReadFrame("arealightasmesh").ToString(), false);
            scene.DefaultLightGain = lightGain;
            scene.MaxPathDepth = maxPathDepth;
            scene.ShadowRayCount = qualitySettings.ShadowRaysPerLight;
            scene.MaxPaths = pathBufferSize;
            scene.sceneData = new SceneDataInfo(geoInfo);
            scene.MaterialProvider = new MaterialInfoContainer(new MaterialInfo() { Kd = new RgbSpectrum(0.5f) }, mtls);
            LoadTextures(mtls, mtls.Length);

            scene.texturesMap = texturesMap;
            scene.OptTextures = this.Textures;

            List<ILight> lights = new List<ILight>();
            ILight envMap = null;
            foreach (var lightEntity in configuration.Lights)
            {
                var light = lightFactory.Create(lightEntity);
                if ((light is BaseInfiniteLight) && envMap == null)
                {
                    envMap = light;
                }
                lights.Add(light);
            }

            if (envMap != null)
            {
                scene.EnvironmentMap = (BaseInfiniteLight)envMap;
            }

            scene.Lights = lights.ToArray();

            Materials = new List<ISurfaceMaterial>();
            foreach (var materialEntity in configuration.Materials)
            {
                Materials.Add(materialFactory.Create(materialEntity));
            }

            scene.SceneGeometry = SceneGeo;

            scene.BuildSceneData();

            if (GlobalConfiguration.Instance.UseSceneCaching)
            {
                var cacheFileName = string.Format("{0}{1}.cache", GlobalConfiguration.Instance.SceneCacheDirectory, frameName);
                if (File.Exists(cacheFileName))
                {
                    scene.Cache = SceneCache.Load(cacheFileName);
                }
                else
                {

                    Tracer.TraceLine("Building bvh cache");
                    var da = new BvhDataAdapter(scene);
                    var cache = new SceneCache { BvhData = da.BuildData() };
                    cache.Save(cacheFileName);
                    scene.Cache = cache;
                }
            }

            if (scene.Lights.Length == 0)
            {
                throw new ApplicationException("No lights in scene");
            }

        }
        private void LoadTextures(IEnumerable<MaterialInfo> matInfos, int matsCount)
        {
            Tracer.TraceLine("Start loading textures");
            texturesMap = new Dictionary<TextureType, int>[matsCount];
            this.Textures = new OptimizedTextureAtlas();


            int materialIndex = 0;
            foreach (var materialInfo in matInfos)
            {
                var textures = new Dictionary<TextureType, int>();

                if (materialInfo.Diffuse != null)
                {
                    ProcessMaterialTexture((ImageTextureInfo)materialInfo.Diffuse, textures, TextureType.Diffuse);
                }

                if (materialInfo.BumpMap != null)
                {
                    ProcessMaterialTexture((ImageTextureInfo)materialInfo.BumpMap, textures, TextureType.Bump);
                }

                if (materialInfo.NormalMap != null)
                {
                    ProcessMaterialTexture((ImageTextureInfo)materialInfo.NormalMap, textures, TextureType.Normal);
                }

                if (materialInfo.Alpha != null && materialInfo.AlphaTexture is ImageTextureInfo)
                {
                    ProcessMaterialTexture((ImageTextureInfo)materialInfo.AlphaTexture, textures, TextureType.Alpha);
                }

                if (materialInfo.Specular != null)
                {
                    ProcessMaterialTexture((ImageTextureInfo)materialInfo.Specular, textures, TextureType.Reflection);
                }
                /*
            else
            {
                materialInfo.BumpMap = new SolidTextureInfo() {Name = "noise"};
                this.Textures.AddTexture(new SolidTextureInfo()
                {
                    Name = "noise"
                });
                textures.Add("noise");
            }*/

                if (textures.Any())
                    texturesMap[materialIndex] = textures;
                materialIndex++;

            }

            Tracer.TraceLine("Complete loading textures");
        }

        private void ProcessMaterialTexture(ImageTextureInfo diff, Dictionary<TextureType, int> textures, TextureType type)
        {
            try
            {
                var texPath = this.FindTexture(diff);
                if (texPath != null)
                {
                    var id = this.Textures.AddTexture(new ImageTextureInfo()
                    {
                        FilePath = texPath,
                        Name = Path.GetFileName(texPath)
                    });
                    textures.Add(type, id);
                }
                else
                {
                    Console.WriteLine("Texture {0} not found!", diff.FilePath);
                }
            }
            catch (Exception ex)
            {
                Tracer.TraceLine("Couldnt load texture {0}", diff.FilePath);
                Tracer.TraceLine(ex.Message);
                Tracer.TraceLine(ex.StackTrace);
            }
        }

        public string FindTexture(ImageTextureInfo ii)
        {
            string filePath = ii.FilePath;
            if (File.Exists(filePath))
                return filePath;
            if (File.Exists(ii.Name))
            {
                return Directory.GetCurrentDirectory() + @"\" + ii.Name;
            }
            string[] textureDirs = GlobalConfiguration.Instance.Get("Renderer.TexturesPaths", new string[0]);
            return (from textureDir in textureDirs.Select(item => item.Trim('\r', '\n', ' ')) where File.Exists(textureDir + "\\" + ii.FilePath) select textureDir + "\\" + ii.FilePath).FirstOrDefault();
        }


        public MaterialInfo[] MaterialInfos { get; set; }
    }
}